/*
 * Instituto Tecnologico de Costa Rica
 * Maestría en Ciencias de la Computación
 * Computer Graphics 2
 *
 * Polygon Renderer
 *
 * Alexander Ujueta
 * Andrés Aguilar
 */

#include "renderer.h"
#include "bresenham.h"
#include "object_loader.h"

//Resolución de la pantalla
int ancho_ventana = 600;
int alto_ventana = 600;

// Parámetros de la interfaz
char* ruta_wireframe;
tipo_proyeccion_t tipo_proyeccion = proyeccion_paralela;
tipo_iluminacion_t tipo_iluminacion  = iluminacion_wireframe;
camara_t camara;
color_t color_objeto;
color_t color_background;
luz_t* luces;
punto_t ancla_rotacion;
eje_t eje_rotacion;
double velocidad_rotacion;

//Variables de rendereo
color_t** buffer = NULL;
objeto_t objeto;
objeto_t objeto_transformado;
long double matriz_transformacion[4][4];
int transformacion_cargada = 0;

void inicializar(void)
{
	int i,j;

	// Asigna la memoria necesaria para el framebuffer
	buffer = (color_t **)malloc(alto_ventana * sizeof(color_t*));
	for (i = 0; i < alto_ventana; i++)
		buffer[i] = (color_t *)malloc(ancho_ventana * sizeof(color_t));

	// Inicializa el framebuffer
	for (i = 0; i < alto_ventana; i++)
		for (j = 0; j < ancho_ventana; j++)
		{
			buffer[i][j].r = color_background.r;
			buffer[i][j].g = color_background.g;
			buffer[i][j].b = color_background.b;
		}

	// Carga el objeto y una copia
	objeto = cargar_objeto(ruta_wireframe);
	objeto_transformado = cargar_objeto(ruta_wireframe);

	//Calcula VRC
	long double norma_N = sqrt((camara.VPN.x * camara.VPN.x) + (camara.VPN.y * camara.VPN.y) + (camara.VPN.z * camara.VPN.z));
	camara.VRC.n.x = camara.VPN.x / norma_N;
	camara.VRC.n.y = camara.VPN.y / norma_N;
	camara.VRC.n.z = camara.VPN.z / norma_N;

	camara.VRC.u.x = (camara.VUP.y * camara.VRC.n.z) - (camara.VUP.z * camara.VRC.n.y);
	camara.VRC.u.y = ((camara.VUP.x * camara.VRC.n.z) - (camara.VUP.z * camara.VRC.n.x)) * -1.0;
	camara.VRC.u.z = (camara.VUP.x * camara.VRC.n.y) - (camara.VUP.y * camara.VRC.n.x);
	long double norma_U = sqrt((camara.VRC.u.x * camara.VRC.u.x) + (camara.VRC.u.y * camara.VRC.u.y) + (camara.VRC.u.z * camara.VRC.u.z));
	camara.VRC.u.x = camara.VRC.u.x / norma_U;
	camara.VRC.u.y = camara.VRC.u.y / norma_U;
	camara.VRC.u.z = camara.VRC.u.z / norma_U;

	camara.VRC.v.x = (camara.VRC.n.y * camara.VRC.u.z) - (camara.VRC.n.z * camara.VRC.u.y);
	camara.VRC.v.y = ((camara.VRC.n.x * camara.VRC.u.z) - (camara.VRC.n.z * camara.VRC.u.x)) * -1.0;
	camara.VRC.v.z = (camara.VRC.n.x * camara.VRC.u.y) - (camara.VRC.n.y * camara.VRC.u.x);
	long double norma_V = sqrt((camara.VRC.v.x * camara.VRC.v.x) + (camara.VRC.v.y * camara.VRC.v.y) + (camara.VRC.v.z * camara.VRC.v.z));
	camara.VRC.v.x = camara.VRC.v.x / norma_V;
	camara.VRC.v.y = camara.VRC.v.y / norma_V;
	camara.VRC.v.z = camara.VRC.v.z / norma_V;

	//Calcula el Center of Window
	camara.CoW.x = (camara.u_min + camara.u_max) / 2.0;
	camara.CoW.y = (camara.v_min + camara.v_max) / 2.0;
	camara.CoW.z = 0.0;

	//Calcula el Direction of Projection
	camara.DoP.x = camara.CoW.x - camara.PRP.x;
	camara.DoP.y = camara.CoW.y - camara.PRP.y;
	camara.DoP.z = camara.CoW.z - camara.PRP.z;
}

void liberar_recursos(void)
{
	int i;
	for (i = 0; i < alto_ventana; i++)
		free(buffer[i]);
	free(buffer);

	if (objeto.superficies != NULL)
		free(objeto.superficies);

	if (objeto.vertices != NULL)
		free(objeto.vertices);

	if (objeto_transformado.superficies != NULL)
		free(objeto_transformado.superficies);

	if (objeto_transformado.vertices != NULL)
		free(objeto_transformado.vertices);

	printf("Recursos liberados");
}

// Cierra la ventana, libera los recursos y detiene el programa
void cerrar_programa(int codigo_retorno)
{
	exit(codigo_retorno);
}

void cargar_transformacion(long double matriz[4][4])
{
	long double nueva_transformacion[4][4];
	int i, j;

	if (transformacion_cargada == 1)
	{
		for (i = 0; i < 4; i++)
			for (j = 0; j < 4; j++)
			{
				nueva_transformacion[i][j] = (matriz[i][0] * matriz_transformacion[0][j]) +
											 (matriz[i][1] * matriz_transformacion[1][j]) +
											 (matriz[i][2] * matriz_transformacion[2][j]) +
											 (matriz[i][3] * matriz_transformacion[3][j]);
			}
	}
	else
	{
		for (i = 0; i < 4; i++)
			for (j = 0; j < 4; j++)
				nueva_transformacion[i][j] = matriz[i][j];
	}

	for (i = 0; i < 4; i++)
		for (j = 0; j < 4; j++)
			matriz_transformacion[i][j] = nueva_transformacion[i][j];

	transformacion_cargada = 1;

}

int clippear_linea(vertice_t origen, vertice_t destino, vertice_t* origen_clippeado, vertice_t* destino_clippeado)
{
	long double x, y, z, t;

	// Iguala la entrada con la salida
	origen_clippeado->x = origen.x;
	origen_clippeado->y = origen.y;
	origen_clippeado->z = origen.z;

	destino_clippeado->x = destino.x;
	destino_clippeado->y = destino.y;
	destino_clippeado->z = destino.z;

	// Clipea los puntos hasta que ambos estén dentro
	while(1)
	{
		unsigned int outcode1 = 0x0, outcode2 = 0x0;

		//Procesa el punto origen
		outcode1 |= origen_clippeado->y > 1  ? 0x1  : 0x0;
		outcode1 |= origen_clippeado->y < -1 ? 0x2  : 0x0;
		outcode1 |= origen_clippeado->x > 1  ? 0x4  : 0x0;
		outcode1 |= origen_clippeado->x < -1 ? 0x8  : 0x0;
		outcode1 |= origen_clippeado->z < -1 ? 0x16 : 0x0;
		outcode1 |= origen_clippeado->z > 0  ? 0x32 : 0x0;

		//Procesa el punto destino
		outcode2 |= destino_clippeado->y > 1  ? 0x1  : 0x0;
		outcode2 |= destino_clippeado->y < -1 ? 0x2  : 0x0;
		outcode2 |= destino_clippeado->x > 1  ? 0x4  : 0x0;
		outcode2 |= destino_clippeado->x < -1 ? 0x8  : 0x0;
		outcode2 |= destino_clippeado->z < -1 ? 0x16 : 0x0;
		outcode2 |= destino_clippeado->z > 0  ? 0x32 : 0x0;

		// Aceptado trivialmente
		if (outcode1 == 0x0 && outcode2 == 0x0)
		{
			return 0;
		}
		// Negado trivialmente
		else if ((outcode1 & outcode2) != 0x0)
		{
			return 1;
		}

		//Elige el punto donde se va a hacer clipping
		unsigned int outcode;
		if (outcode1 != 0x0)
			outcode = outcode1;
		else
			outcode = outcode2;


		// Clipping contra z = 0
		if ((outcode & 0x32) == 0x32)
		{
			z = 0.0;
			t = (0.0 - origen_clippeado->z)/(destino_clippeado->z - origen_clippeado->z);
			x = origen_clippeado->x + t * (destino_clippeado->x - origen_clippeado->x);
			y = origen_clippeado->y + t * (destino_clippeado->y - origen_clippeado->y);
		}
		// Clipping contra z = -1
		else if ((outcode & 0x16) == 0x16)
		{
			z = -1.0;
			t = (-1.0 - origen_clippeado->z)/(destino_clippeado->z - origen_clippeado->z);
			x = origen_clippeado->x + t * (destino_clippeado->x - origen_clippeado->x);
			y = origen_clippeado->y + t * (destino_clippeado->y - origen_clippeado->y);
		}
		// Clipping contra x = -1
		else if ((outcode & 0x8) == 0x8)
		{
			x = -1.0;
			t = (-1.0 - origen_clippeado->x)/(destino_clippeado->x - origen_clippeado->x);
			y = origen_clippeado->y + t * (destino_clippeado->y - origen_clippeado->y);
			z = origen_clippeado->z + t * (destino_clippeado->z - origen_clippeado->z);
		}
		// Clipping contra x = 1
		else if ((outcode & 0x4) == 0x4)
		{
			x = 1.0;
			t = (1.0 - origen_clippeado->x)/(destino_clippeado->x - origen_clippeado->x);
			y = origen_clippeado->y + t * (destino_clippeado->y - origen_clippeado->y);
			z = origen_clippeado->z + t * (destino_clippeado->z - origen_clippeado->z);
		}
		// Clipping contra y = -1
		else if ((outcode & 0x2) == 0x2)
		{
			y = -1.0;
			t = (-1.0 - origen_clippeado->y)/(destino_clippeado->y - origen_clippeado->y);
			x = origen_clippeado->x + t * (destino_clippeado->x - origen_clippeado->x);
			z = origen_clippeado->z + t * (destino_clippeado->z - origen_clippeado->z);
		}
		// Clipping contra y = 1
		else if ((outcode & 0x1) == 0x1)
		{
			y = 1.0;
			t = (1.0 - origen_clippeado->y)/(destino_clippeado->y - origen_clippeado->y);
			x = origen_clippeado->x + t * (destino_clippeado->x - origen_clippeado->x);
			z = origen_clippeado->z + t * (destino_clippeado->z - origen_clippeado->z);
		}

		// Setea el nuevo punto clipeado
		if (outcode1 != 0x0)
		{
			origen_clippeado->x = x;
			origen_clippeado->y = y;
			origen_clippeado->z = z;
		}
		else
		{
			destino_clippeado->x = x;
			destino_clippeado->y = y;
			destino_clippeado->z = z;
		}
	}

	return 1;
}

void proyectar_objeto(void)
{
	int i;

	//Vértices clippeados
	vertice_t vertice1_2, vertice1_3;
	vertice_t vertice2_1, vertice2_3;
	vertice_t vertice3_1, vertice3_2;

	//Variables de control del clipping
	int linea1, linea2, linea3;

	//Dibuja cada superficie del objeto
	for (i = 0; i < objeto_transformado.num_superficies; i++)
	{
		vertice_t vertice1 = objeto_transformado.vertices[objeto_transformado.superficies[i].indices[0]];
		vertice_t vertice2 = objeto_transformado.vertices[objeto_transformado.superficies[i].indices[1]];
		vertice_t vertice3 = objeto_transformado.vertices[objeto_transformado.superficies[i].indices[2]];

		//Clipea las líneas del triángulo
		linea1 = clippear_linea(vertice1, vertice2, &vertice1_2, &vertice2_1);
		linea2 = clippear_linea(vertice2, vertice3, &vertice2_3, &vertice3_2);
		linea3 = clippear_linea(vertice3, vertice1, &vertice3_1, &vertice1_3);

		// Mapea los puntos a coordenadas de frame buffer y dibuja la línea
		if (linea1 != 1)
		{
			vertice1_2.x = ((vertice1_2.x + 1.0)/2.0) * (long double)alto_ventana;
			vertice1_2.y = ((vertice1_2.y + 1.0)/2.0) * (long double)ancho_ventana;

			vertice2_1.x = ((vertice2_1.x + 1.0)/2.0) * (long double)alto_ventana;
			vertice2_1.y = ((vertice2_1.y + 1.0)/2.0) * (long double)ancho_ventana;

			line_Bresenham(vertice1_2.x,vertice1_2.y,vertice2_1.x,vertice2_1.y,buffer,color_objeto);
		}
		if (linea2 != 1)
		{
			vertice2_3.x = ((vertice2_3.x + 1.0)/2.0) * (long double)alto_ventana;
			vertice2_3.y = ((vertice2_3.y + 1.0)/2.0) * (long double)ancho_ventana;

			vertice3_2.x = ((vertice3_2.x + 1.0)/2.0) * (long double)alto_ventana;
			vertice3_2.y = ((vertice3_2.y + 1.0)/2.0) * (long double)ancho_ventana;

			line_Bresenham(vertice2_3.x,vertice2_3.y,vertice3_2.x,vertice3_2.y,buffer,color_objeto);
		}
		if (linea3 != 1)
		{
			vertice3_1.x = ((vertice3_1.x + 1.0)/2.0) * (long double)alto_ventana;
			vertice3_1.y = ((vertice3_1.y + 1.0)/2.0) * (long double)ancho_ventana;

			vertice1_3.x = ((vertice1_3.x + 1.0)/2.0) * (long double)alto_ventana;
			vertice1_3.y = ((vertice1_3.y + 1.0)/2.0) * (long double)ancho_ventana;

			line_Bresenham(vertice3_1.x,vertice3_1.y,vertice1_3.x,vertice1_3.y,buffer,color_objeto);
		}
	}
}

void transformar_objeto(void)
{
	// Variables generales para la transformación
	int i;
	long double vector_transformado[4];
	punto_t PRP_transformado;


	// SECCION 1: TRANSFORMACIONES GENERALES PARA PARALELAS Y PERSPECTIVAS

	// Calcula la matriz para trasladar VRP al origen
	long double matriz_traslado_VRP[4][4] = {{1.0,0.0,0.0,(-1.0 * camara.VRP.x)},
											 {0.0,1.0,0.0,(-1.0 * camara.VRP.y)},
											 {0.0,0.0,1.0,(-1.0 * camara.VRP.z)},
											 {0.0,0.0,0.0,1.0}};
	cargar_transformacion(matriz_traslado_VRP);

	// Calcula la matriz para rotar VRC a XYZ
	long double matriz_rotacion[4][4] = {{camara.VRC.u.x, camara.VRC.u.y, camara.VRC.u.z, 0.0},
										 {camara.VRC.v.x, camara.VRC.v.y, camara.VRC.v.z, 0.0},
										 {camara.VRC.n.x, camara.VRC.n.y, camara.VRC.n.z, 0.0},
										 {0.0,0.0,0.0,1.0}};
	cargar_transformacion(matriz_rotacion);


	// SECCIÓN 2: TRANSFORMACIONES PARA PROYECCIONES PARALELAS
	if (tipo_proyeccion == proyeccion_paralela)
	{
		// Calcula la matriz para hacer shear
		long double SHx = -1.0 * (camara.DoP.x/camara.DoP.z);
		long double SHy = -1.0 * (camara.DoP.y/camara.DoP.z);
		long double matriz_shear[4][4] = {{1.0,0.0,SHx,0.0},
										  {0.0,1.0,SHy,0.0},
										  {0.0,0.0,1.0,0.0},
										  {0.0,0.0,0.0,1.0}};
		cargar_transformacion(matriz_shear);

		// Calcula la matriz para trasladar el CoW al origen y el Front Plane al View Plane
		long double matriz_traslado_FP[4][4] = {{1.0,0.0,0.0,(-1.0 * camara.CoW.x)},
												{0.0,1.0,0.0,(-1.0 * camara.CoW.y)},
												{0.0,0.0,1.0,(-1.0 * camara.F)},
												{0.0,0.0,0.0,1.0}};
		cargar_transformacion(matriz_traslado_FP);

		// Calcula la matriz para escalar Z y traer el BP a -1
		long double Sx = 2.0 / (camara.u_max - camara.u_min);
		long double Sy = 2.0 / (camara.v_max - camara.v_min);
		long double Sz = -1.0 / (camara.B - camara.F);
		long double matriz_escala[4][4] = {{Sx,0.0,0.0,0.0},
										   {0.0,Sy,0.0,0.0},
										   {0.0,0.0,Sz,0.0},
										   {0.0,0.0,0.0,1.0}};
		cargar_transformacion(matriz_escala);
	}


	// SECCIÓN 3: TRANSFORMACIONES PARA PROYECCIONES EN PERSPECTIVA
	if (tipo_proyeccion == proyeccion_perspectiva)
	{
		//Aplica las transformaciones al PRP
		for (i = 0; i < 4; i++)
			vector_transformado[i] = (matriz_transformacion[i][0] * camara.PRP.x ) +
									 (matriz_transformacion[i][1] * camara.PRP.y ) +
									 (matriz_transformacion[i][2] * camara.PRP.z ) +
									 (matriz_transformacion[i][3] * 1.0);

		PRP_transformado.x = vector_transformado[0];
		PRP_transformado.y = vector_transformado[1];
		PRP_transformado.z = vector_transformado[2];

		//Calcula el Direction of Projection con el nuevo PRP
		camara.DoP.x = camara.CoW.x - PRP_transformado.x;
		camara.DoP.y = camara.CoW.y - PRP_transformado.y;
		camara.DoP.z = camara.CoW.z - PRP_transformado.z;

		// Calcula la matriz para hacer shear. NO SE CARGA TODAVÍA
		long double SHx = -1.0 * (camara.DoP.x/camara.DoP.z);
		long double SHy = -1.0 * (camara.DoP.y/camara.DoP.z);
		long double matriz_shear[4][4] = {{1.0,0.0,SHx,0.0},
										  {0.0,1.0,SHy,0.0},
										  {0.0,0.0,1.0,0.0},
										  {0.0,0.0,0.0,1.0}};

		// Calcula la matriz para trasladar el PRP al origen
		long double matriz_traslado_PRP[4][4] = {{1.0,0.0,0.0,(-1.0 * PRP_transformado.x)},
												 {0.0,1.0,0.0,(-1.0 * PRP_transformado.y)},
												 {0.0,0.0,1.0,(-1.0 * PRP_transformado.z)},
												 {0.0,0.0,0.0,1.0}};
		cargar_transformacion(matriz_traslado_PRP);

		// Carga la transformación de Shear que ya se calculó
		cargar_transformacion(matriz_shear);

		// Calcula VRP luego del traslado de PRP y el Shear
		punto_t VRP_transformado;
		VRP_transformado.x = PRP_transformado.x + SHx * PRP_transformado.z;
		VRP_transformado.y = PRP_transformado.y + SHx * PRP_transformado.z;
		VRP_transformado.z = PRP_transformado.z;

		// Calcula la matriz para escalar Z y traer el BP a -1
		long double Sx = (2.0 * VRP_transformado.z) / ((camara.u_max - camara.u_min) * (VRP_transformado.z + camara.B));
		long double Sy = (2.0 * VRP_transformado.z) / ((camara.v_max - camara.v_min) * (VRP_transformado.z + camara.B));
		long double Sz = -1.0 / (VRP_transformado.z + camara.B);
		long double matriz_escala[4][4] = {{Sx,0.0,0.0,0.0},
										   {0.0,Sy,0.0,0.0},
										   {0.0,0.0,Sz,0.0},
										   {0.0,0.0,0.0,1.0}};
		cargar_transformacion(matriz_escala);
	}

	// SECCIÓN 4: APLICACIÓN DE LA TRANSFORMACIÓN DE NORMALIZACIÓN A TODOS LOS PUNTOS
	for (i=0; i < objeto.num_vertices; i++)
	{
		long double vector_transformado[4];

		int j;
		for (j = 0; j < 4; j++)
			vector_transformado[j] = (matriz_transformacion[j][0] * objeto.vertices[i].x ) +
									 (matriz_transformacion[j][1] * objeto.vertices[i].y ) +
									 (matriz_transformacion[j][2] * objeto.vertices[i].z ) +
									 (matriz_transformacion[j][3] * 1.0);

		objeto_transformado.vertices[i].x = vector_transformado[0];
		objeto_transformado.vertices[i].y = vector_transformado[1];
		objeto_transformado.vertices[i].z = vector_transformado[2];
	}

	transformacion_cargada = 0;
}

void display(void)
{
	int i, j;

	//Procesa el objeto para poder desplegarlo
	transformar_objeto();
	proyectar_objeto();

	//Dibuja el frame buffer resultante en la pantalla
	for (i = 0; i < alto_ventana; i++)
		for (j = 0; j < ancho_ventana; j++)
		{
			glColor3f (buffer[i][j].r, buffer[i][j].g, buffer[i][j].b);
			glBegin(GL_POINTS);
			glVertex2i(i,j);
			glEnd();
		}

	glutSwapBuffers();
}

//Método que llama la interfaz para pintar en el framebuffer
color_t** render(char* ruta_wireframeP, tipo_proyeccion_t tipo_proyeccionP, tipo_iluminacion_t tipo_iluminacionP,
				 camara_t camaraP, color_t color_objetoP, color_t color_backgroundP, luz_t* lucesP,
				 punto_t ancla_rotacionP, eje_t eje_rotacionP, double velocidad_rotacionP)
{
	ruta_wireframe = ruta_wireframeP;
	tipo_proyeccion = tipo_proyeccionP;
	tipo_iluminacion = tipo_iluminacionP;
	camara = camaraP;
	color_objeto = color_objetoP;
	color_background = color_backgroundP;
	luces = lucesP;
	ancla_rotacion = ancla_rotacionP;
	eje_rotacion = eje_rotacionP;
	velocidad_rotacion = velocidad_rotacionP;

	inicializar();

	//Procesa el objeto para poder desplegarlo
	transformar_objeto();
	proyectar_objeto();

	return buffer;
}

// Función principal del programa, inicializa glew y crea la ventana
int main(int argc, char** argv)
{
	//VALORES POR DEFECTO. QUITAR CUANDO SE PEGUE CON LA INTERFAZ
	ruta_wireframe = "res/cow.obj";
	tipo_proyeccion = proyeccion_paralela;
	tipo_iluminacion  = iluminacion_wireframe;

	camara.VRP.x = 0.9;
	camara.VRP.y = 0.0;
	camara.VRP.z = 0.0;

	camara.VPN.x = 0.0;
	camara.VPN.y = 0.0;
	camara.VPN.z = 1.0;

	camara.VUP.x = 0.0;
	camara.VUP.y = 1.0;
	camara.VUP.z = 0.0;

	camara.offset = 0.0;

	camara.u_min = -6.0;
	camara.u_max = 6.0;
	camara.v_min = -6.0;
	camara.v_max = 6.0;

	camara.F = 100.0;
	camara.B = -100.0;

	camara.PRP.x = 0.0;
	camara.PRP.y = 0.0;
	camara.PRP.z = -1.0;

	color_objeto.r = 1.0;
	color_objeto.g = 1.0;
	color_objeto.b = 1.0;

	color_background.r = 0.0;
	color_background.g = 0.0;
	color_background.b = 0.0;


	inicializar();

	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
	glutInitWindowSize(alto_ventana, ancho_ventana);
	glutCreateWindow("Polygon Renderer - Ujueta - Aguilar");
	glutDisplayFunc(display);
	glutIdleFunc(display);
	gluOrtho2D(-0.5, alto_ventana-0.5, -0.5, ancho_ventana-0.5);

	glewInit();
	if (!GLEW_VERSION_2_0) {
		printf("OpenGL 2.0 no está disponible\n");
		cerrar_programa(1);
	}

	atexit(liberar_recursos);
	glutMainLoop();

	return 0;
}
